Skip to main content

Declarative Macros

Declarative macros (also called macro_rules macros) let you define pattern-based code generation. You describe what input patterns look like and what code they expand into.

They are:

  • Matched at compile time
  • Purely syntactic (no runtime cost)
  • Great for eliminating repetitive code

Think of them as smart search-and-replace rules, but with structure and safety.

Basic Structure

macro_rules! macro_name {
(pattern) => {
expansion
};
}

You can define multiple rules inside one macro.

Example 1: A Simple Macro

macro_rules! say_hello {
() => {
println!("Hello, world!");
};
}

fn main() {
say_hello!();
}

How it works:

  • () matches no arguments.
  • When the compiler sees say_hello!(), it replaces it with:
println!("Hello, world!");

Macro Variables and Fragment Specifiers

You can capture parts of the input using macro variables:

macro_rules! print_value {
($x:expr) => {
println!("The value is: {}", $x);
};
}

Here:

  • $x is a variable.
  • expr is a fragment specifier (tells Rust what kind of syntax to match).

Common fragment specifiers:

SpecifierMatches
exprExpression
identIdentifier
tyType
patPattern
stmtStatement
blockBlock { ... }
itemItem (fn, struct, etc.)
literalLiteral value
pathPath like std::fmt::Debug
ttToken tree (most flexible)

Example 2: Macro with One Argument

macro_rules! square {
($x:expr) => {
$x * $x
};
}

fn main() {
let a = square!(4);
let b = square!(3 + 2);
println!("{}, {}", a, b);
}

Expansion:

  • square!(4)4 * 4
  • square!(3 + 2)(3 + 2) * (3 + 2) (conceptually)

Example 3: Multiple Patterns (Overloading)

macro_rules! log {
($msg:expr) => {
println!("LOG: {}", $msg);
};
($fmt:expr, $($arg:tt)*) => {
println!(concat!("LOG: ", $fmt), $($arg)*);
};
}

fn main() {
log!("Hello");
log!("x = {}, y = {}", 10, 20);
}

How it works:

  • First rule matches one expression.
  • Second rule matches a format string plus any number of extra tokens (((`arg:tt)*`).

Repetition Syntax

Repetition lets you match multiple inputs.

$(pattern),*   // zero or more, separated by commas
$(pattern),+ // one or more
$(pattern)? // zero or one

Example 4: Variadic Macro (Like vec!)

Let’s reimplement a simplified vec! macro:

macro_rules! my_vec {
() => {
Vec::new()
};
($($x:expr),+ $(,)?) => {
{
let mut v = Vec::new();
$(
v.push($x);
)+
v
}
};
}

fn main() {
let a = my_vec![];
let b = my_vec![1, 2, 3];
let c = my_vec![10, 20, 30,];
}
  • () handles empty input.
  • $($x:expr),+ matches one or more expressions separated by commas.
  • $(,)? allows an optional trailing comma.
  • $()+ repeats the code for each matched $x.

Example 5: Generating Code (Struct + Impl)

macro_rules! make_struct {
($name:ident) => {
struct $name;

impl $name {
fn new() -> Self {
$name
}
}
};
}

make_struct!(Foo);

fn main() {
let f = Foo::new();
}

Expansion:

struct Foo;

impl Foo {
fn new() -> Self {
Foo
}
}

This shows how macros can generate items, not just expressions.

Hygiene (Why Macros Are Safe)

Rust macros are hygienic, meaning:

  • Variables defined inside a macro won’t accidentally clash with variables outside it.
  • Names resolve to the correct scope.
macro_rules! make_var {
() => {
let x = 10;
println!("{}", x);
};
}

fn main() {
let x = 5;
make_var!(); // prints 10, not 5
}

The macro’s x is distinct from the outer x.

When to Use macro_rules!

Use declarative macros when:

  • You want to eliminate repetitive boilerplate.
  • You need syntax that functions can’t express (e.g., variadic arguments).
  • You want zero runtime cost abstractions.

Avoid them when:

  • The logic becomes too complex or unreadable.
  • A function, trait, or generic type would work just as well.